//
//  MyTextViewDelegate.m
//  cocoaembed
//
//  Created by Andrew Barry on 10/03/08.
//  Copyright 2008 __MyCompanyName__. All rights reserved.
//

#import "MyTextViewDelegate.h"

static NSImage *greenArrow;

static int  countLines(NSString *str) {
	const char *text = [str UTF8String];
	int pos = 0;
	int lineCount = 1;
	while (text[pos]) {
		char ch = text[pos++];
		if (ch == 10)
			lineCount++;
		else if (ch == 13) {
			if (text[pos] == 10)
				pos++;
		}
	}
	return lineCount;
}

static int posToLine(NSString *str, int offset) {
	int index = 0, count = [str length];
	int lineNumber = 1;
	
	while (index < count) {
		NSRange span = [str lineRangeForRange:NSMakeRange(index, 0)];
	
		if (offset >= span.location && offset < (span.location + span.length))
			break;
		index = NSMaxRange(span);
		if (index == count)
			break;
		lineNumber++;
	}
	return lineNumber;
}

int lineToPos(NSString *str, int line) {
	int index = 0, count = [str length];
	int lineNumber = 1;
	
	while (index < count) {
		if (line == lineNumber)
			return index;
		NSRange span = [str lineRangeForRange:NSMakeRange(index, 0)];
		
		index = NSMaxRange(span);
		lineNumber++;
	}
	return count;
}

@interface MyTextViewRuler : NSRulerView {
	@public NSTextView *textview;
	@public MyTextViewDelegate *textviewDelegate;
}

@end
@implementation MyTextViewRuler
- (void)drawHashMarksAndLabelsInRect:(NSRect)aRect
{
	NSRect		bounds = [self bounds];
	NSRange nullRange = NSMakeRange(NSNotFound, 0);
	unsigned rectCount;

	MyTextViewDelegate *delegate = textviewDelegate;
	
	NSFont *font = [NSFont labelFontOfSize:[NSFont systemFontSizeForControlSize:NSMiniControlSize]];
	NSColor *textColor = [NSColor colorWithCalibratedWhite:0.42 alpha:1.0];
	NSColor *debugTextColor = [NSColor colorWithCalibratedWhite:1.0 alpha:1.0];
	NSColor *breakpointColor = [NSColor colorWithCalibratedRed:1.0 green:0.2 blue:0.2 alpha:1.0];
	NSDictionary *textAttributes = [NSDictionary dictionaryWithObjectsAndKeys:
				font, NSFontAttributeName, 
				textColor, NSForegroundColorAttributeName,
				nil];
	NSDictionary *debugTextAttributes = [NSDictionary dictionaryWithObjectsAndKeys:
									font, NSFontAttributeName, 
									debugTextColor, NSForegroundColorAttributeName,
									nil];
	
	NSLayoutManager *layoutManager = [textview layoutManager];
	NSTextContainer *container = [textview textContainer];
	NSScrollView *scrollView = [self scrollView];
	NSView *contentView = [scrollView contentView];
	NSRect visibleRect = [contentView bounds];
	NSArray *currentBreakpoints = [delegate breakpoints];
	if (!currentBreakpoints)
		return;
	NSSet *breakpointSet = [NSSet setWithArray: currentBreakpoints];
	int runMarker = [delegate runMarker];
	
	NSRange glyphRange = [layoutManager glyphRangeForBoundingRect:visibleRect inTextContainer:container];
	NSRange range = [layoutManager characterRangeForGlyphRange:glyphRange actualGlyphRange:NULL];
	int rangeStart = range.location;
	int rangeEnd = range.location + range.length;
	NSString *string = [textview string];
	unsigned numberOfLines, index, stringLength = [string length];
	for (index = 0, numberOfLines = 0; index < stringLength && index <= rangeEnd; numberOfLines++)
	{
		NSRange span = [string lineRangeForRange:NSMakeRange(index, 0)];
		int spanStart = span.location;
		int spanEnd = span.location + span.length;
		if (rangeEnd >= spanStart && rangeStart <= spanEnd) {
			NSRectArray rects = [layoutManager rectArrayForCharacterRange:NSMakeRange(index, 0)
								 withinSelectedCharacterRange:nullRange
											  inTextContainer:container
													rectCount:&rectCount];
			if (rectCount > 0) {
				int ypos = rects[0].origin.y - visibleRect.origin.y;
				NSString *labelText = [NSString stringWithFormat:@"%d", numberOfLines + 1];
				if ([breakpointSet containsObject:[NSNumber numberWithInt:numberOfLines + 1]]) {
					[breakpointColor setFill];
					NSRectFill(NSMakeRect(0, ypos, bounds.size.width, NSHeight(rects[0])));
					[labelText drawInRect: NSMakeRect(2, ypos + 1, bounds.size.width, NSHeight(rects[0]))
						   withAttributes: debugTextAttributes];
				} else {
					[labelText drawInRect: NSMakeRect(2, ypos + 1, bounds.size.width, NSHeight(rects[0]))
						   withAttributes: textAttributes];
				}
				if ((numberOfLines + 1) == runMarker) {
					if (!greenArrow) {
						NSString *imagePath = [[NSBundle mainBundle] pathForResource:@"greenArrow" ofType:@"png"];
						greenArrow = [[NSImage alloc] initWithContentsOfFile:imagePath];
						[greenArrow setFlipped:YES];
					}
					[greenArrow drawAtPoint:NSMakePoint(6, ypos + 5) fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:1.0];
				}
			}
			
		}
		
		index = NSMaxRange(span);
	}
}
@end

@implementation MyTextViewDelegate
- (void)awakeFromNib
{
    // register self as textstorage delegate
    [[textview textStorage] setDelegate:self];
	
	MyTextViewRuler *testRuler = [[MyTextViewRuler alloc] initWithScrollView:scrollview orientation:NSVerticalRuler];
	testRuler->textview = [textview retain];
	testRuler->textviewDelegate = [self retain];
	
	textRuler = testRuler;
	
	[scrollview setVerticalRulerView:testRuler];
	[scrollview setHasVerticalRuler:YES]; 
    [scrollview setRulersVisible:YES];
	
	if (!breakpoints)
		breakpoints = [[NSMutableArray alloc] init];
}

-(NSMutableArray *)breakpoints {
	if (!breakpoints)
		breakpoints = [[NSMutableArray alloc] init];

	return breakpoints;
}

- (BOOL)textView:(NSTextView *)aTextView doCommandBySelector:(SEL)aSelector
{
	if (aSelector == @selector(insertNewline:)) {
		NSString *str = [aTextView string];
		NSArray *ranges = [aTextView selectedRanges];
		NSValue *val = (NSValue *) [ranges objectAtIndex:0];
		NSRange range = [val rangeValue];
		int pos = range.location;
		int len = [str length];

		while (pos > 0) {
			unichar ch = [str characterAtIndex: pos-1];
			if (ch == 13 || ch == 10)
				break;
			pos--;
		}
		int base = pos;
		while (pos < len) {
			unichar ch = [str characterAtIndex: pos];
			if (ch == ' ' || ch == '\t')
				pos++;
			else
				break;
		}
		if (pos > base) {
			NSString *leading = [str substringWithRange:NSMakeRange(base, pos - base)];
			NSString *insertText = [@"\n" stringByAppendingString: leading];
			[aTextView insertText:insertText];
		} else
			[aTextView insertText: @"\n"];

		return YES;
	}
	
	return NO;
}

- (BOOL)textView:(NSTextView *)textView shouldChangeTextInRanges:(NSArray *)affectedRanges replacementStrings:(NSArray *)replacementStrings {
	int rangeCount = [affectedRanges count];
	NSString *str = [textView string];

	// do brace flashing
	if (rangeCount == 1) {
		NSString *replacementString = (NSString *) [replacementStrings objectAtIndex:0];
		NSRange affectedCharRange = [(NSValue *) [affectedRanges objectAtIndex:0] rangeValue];
		if ([replacementString isEqualToString: @"}"]) {
			int pos = affectedCharRange.location;
			int depth = 0;
			while (pos > 0) {
				--pos;
				unichar ch = [str characterAtIndex: pos];
				if (ch == '{') {
					if (depth == 0) {
						[self performSelector:@selector(flashAh:) withObject: [NSValue valueWithRange:NSMakeRange(pos, 1)] afterDelay:0.01];
						return YES;
					} else
						depth--;
				} else if (ch == '}') {
					depth++;
				}
			}
		}
	}
	
	// shift breakpoints if necessary
	int rangeIdx;
	for (rangeIdx = 0; rangeIdx < rangeCount; rangeIdx++) {
		NSString *replacementString = (NSString *) [replacementStrings objectAtIndex:rangeIdx];
		NSRange affectedCharRange = [(NSValue *) [affectedRanges objectAtIndex:rangeIdx] rangeValue];
		NSString *oldString = [str substringWithRange:affectedCharRange];
		int oldLineCount = countLines(oldString);
		int newLineCount = countLines(replacementString);
		if (oldLineCount != newLineCount) {
			int baseLine = posToLine(str, affectedCharRange.location);
			int bpIdx = 0;
			while (bpIdx < [breakpoints count]) {
				int bpLine = [[breakpoints objectAtIndex:bpIdx] intValue];
				bool bRemove = false;
				if (bpLine > baseLine) {
					if (bpLine < (baseLine + oldLineCount))
						bRemove = true;
					else
						[breakpoints replaceObjectAtIndex:bpIdx withObject:[NSNumber numberWithInt:bpLine + newLineCount - oldLineCount]];
				}
				if (bRemove)
					[breakpoints removeObjectAtIndex:bpIdx];
				else
					bpIdx++;
			}
		}
	}

	[textRuler setNeedsDisplay:YES];
	return YES;
}

-(void) codeLoaded {
	[textRuler setNeedsDisplay:YES];
}

-(void) toggleBreakpoint {
	int pos = [textview selectedRange].location;
	int line = posToLine([textview string], pos);
	if ([breakpoints containsObject:[NSNumber numberWithInt: line]])
		[breakpoints removeObject:[NSNumber numberWithInt: line]];
	else
		[breakpoints addObject:[NSNumber numberWithInt: line]];
	[textRuler setNeedsDisplay:YES];
}

-(void) setRunMarker: (int) lineno {
	runMarker = lineno;
	[textRuler setNeedsDisplay:YES];
}

-(int) runMarker {
	return runMarker;
}

- (void) flashAh:(NSValue *) range
{
	[textview showFindIndicatorForRange:[range rangeValue]];
}

/*
* FSM
* 0 - neutral
* 1 - ident
* 2 - number
*/
int lexPos;
int lexState;
int lexBase;
const char *lexBaseText;
NSTextStorage *lexStorage;
int lexLastHighlight;

NSColor *colonIdentCol, *keywordCol, *intConstCol, *commentCol;
//NSFont *stdFont, *boldFont, *italFont, *sysFont;
//static NSCharacterSet *fontCoverage;

#define EDITORFONT @"Bitstream Vera Sans Mono"
//#define EDITORFONT @"Times"
/*
void applyFontToRange(int base, int len, NSFont *f) {
	NSString *text = [lexStorage string];
	int p = 0;
	int mode = 0;
	for (int idx = 0; idx < len; idx++) {
		unichar ch = [text characterAtIndex:idx + base];
		if (![fontCoverage characterIsMember:ch]) {
			if (mode == 0) {
				if (p != idx) {
					[lexStorage addAttribute:NSFontAttributeName
									   value:f
									   range:NSMakeRange(p + base, idx - p)];
				}
				p = idx;
				mode = 1;
			}
		} else {
			if (mode == 1) {
				if (p != idx) {
					[lexStorage addAttribute:NSFontAttributeName
									   value:sysFont
									   range:NSMakeRange(p + base, idx - p)];
				}
				p = idx;
				mode = 0;
			}
		}
	}
	if (p != len) {
		[lexStorage addAttribute:NSFontAttributeName
						   value: mode == 0 ? f : sysFont
						   range:NSMakeRange(p + base, len - p)];
	}
}
*/
void lexFoundToken(int base, int len, int col) {
	NSColor *color;
	if (colonIdentCol == nil) {
		colonIdentCol = [[NSColor colorWithDeviceRed: 0.5 green:0.5 blue:1.0 alpha:1.0] retain];
		keywordCol = [[NSColor colorWithDeviceRed: 0 green:0 blue:0.6 alpha:1.0] retain];
		intConstCol = [[NSColor colorWithDeviceRed: 0.6 green:0 blue:0 alpha:1.0] retain];
		commentCol = [[NSColor colorWithDeviceRed: 0 green:0.5 blue:0 alpha:1.0] retain];
//		stdFont = [[[NSFontManager sharedFontManager] fontWithFamily: EDITORFONT traits: 0 weight: 5 size: 12] retain];
//		italFont = [[[NSFontManager sharedFontManager] fontWithFamily: EDITORFONT traits: NSItalicFontMask weight: 5 size: 12] retain];
//		boldFont = [[[NSFontManager sharedFontManager] fontWithFamily: EDITORFONT traits: 0 weight: 9 size: 12] retain];
//		fontCoverage = [[stdFont coveredCharacterSet] retain];
	}
/*
	if (base > lexLastHighlight) {
		[lexStorage addAttribute:NSFontAttributeName
						   value:stdFont
						   range:NSMakeRange(lexLastHighlight, base - lexLastHighlight)];
	}
	*/
	
//	NSFont *font = stdFont;
	if (col == 1)
		color = intConstCol;
	else if (col == 2) {
		color = keywordCol;
//		font = boldFont;
	} else if (col == 3) {
		color = [NSColor blackColor];
//		font = italFont;
	}
	else if (col == 4)
		color = intConstCol;
	else if (col == 8)
		color = commentCol;
	else
		color = [NSColor blackColor];
	[lexStorage addAttribute:NSForegroundColorAttributeName
						value:color
						range:NSMakeRange(base, len)];
	lexLastHighlight = base + len;
}

void lexFoundIdent(int base, int len) {
	if (len == 2 && (!strncmp(lexBaseText, "if", 2) || !strncmp(lexBaseText, "do", 2) || !strncmp(lexBaseText, "in", 2)))
		lexFoundToken(base, len, 2);
	else if (len == 3 && (!strncmp(lexBaseText, "nil", 3) || !strncmp(lexBaseText, "for", 3) || !strncmp(lexBaseText, "var", 3) || !strncmp(lexBaseText, "new", 3) || !strncmp(lexBaseText, "try", 3)))
		lexFoundToken(base, len, 2);
	else if (len == 4 && (!strncmp(lexBaseText, "void", 4) || !strncmp(lexBaseText, "case", 4) || !strncmp(lexBaseText, "else", 4) || !strncmp(lexBaseText, "this", 4) || !strncmp(lexBaseText, "true", 4) || !strncmp(lexBaseText, "with", 4)))
		lexFoundToken(base, len, 2);
	else if (len == 5 && (!strncmp(lexBaseText, "false", 5) || !strncmp(lexBaseText, "throw", 5) || !strncmp(lexBaseText, "while", 5) || !strncmp(lexBaseText, "break", 5) || !strncmp(lexBaseText, "catch", 5)))
		lexFoundToken(base, len, 2);
	else if (len == 6 && (!strncmp(lexBaseText, "return", 6) || !strncmp(lexBaseText, "delete", 6) || !strncmp(lexBaseText, "switch", 6) || !strncmp(lexBaseText, "typeof", 6)))
		lexFoundToken(base, len, 2);
	else if (len == 7 && (!strncmp(lexBaseText, "default", 7) || !strncmp(lexBaseText, "finally", 7)))
		lexFoundToken(base, len, 2);
	else if (len == 8 && (!strncmp(lexBaseText, "function", 8) || !strncmp(lexBaseText, "continue", 8) || !strncmp(lexBaseText, "debugger", 8)))
		lexFoundToken(base, len, 2);
	else if (len == 10 && (!strncmp(lexBaseText, "instanceof", 10)))
		lexFoundToken(base, len, 2);
	else
		lexFoundToken(base, len, 0);
}

void lexComplete() {
	if (lexState == 1)
		lexFoundIdent(lexBase, lexPos - lexBase);
	else if (lexState == 2)
		lexFoundToken(lexBase, lexPos - lexBase, 1);
	else if (lexState == 3)
		lexFoundToken(lexBase, lexPos - lexBase, 3);
	else if (lexState >=4 && lexState <= 6)
		lexFoundToken(lexBase, lexPos - lexBase, 4);
	else if (lexState >= 8 && lexState <= 11)
		lexFoundToken(lexBase, lexPos - lexBase, 8);
//NSFontAttributeName
	if (lexPos > lexLastHighlight) {
		[lexStorage addAttribute:NSForegroundColorAttributeName
							value:[NSColor blackColor]
							range:NSMakeRange(lexLastHighlight, lexPos - lexLastHighlight)];
	}
}

void handleLeadingChar(int ch, const char *txt) {
	lexState = 0;
	if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch =='_') {
		lexState = 1;
		lexBase = lexPos;
		lexBaseText = txt;
	}
	else if (ch >= '0' && ch <= '9') {
		lexState = 2;
		lexBase = lexPos;
		lexBaseText = txt;
	} else if (ch == '"') {
		lexState = 4;
		lexBase = lexPos;
		lexBaseText = txt;
	} else if (ch == '/') {
		if (txt[1] == '/') {
			lexState = 8;
			lexBase = lexPos;
			lexBaseText = txt;
		} else if (txt[1] == '*') {
			lexState = 9;
			lexBase = lexPos;
			lexBaseText = txt;
		}
	}
}

void lexAddCh(int ch, const char *txt) {
	switch (lexState) {
		case 0:
			handleLeadingChar(ch, txt);
			break;
		case 1:
			if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch =='_' || (ch >= '0' && ch <= '9'))
				lexState = 1;
			else {
				lexFoundIdent(lexBase, lexPos - lexBase);
				handleLeadingChar(ch, txt);
			}
			break;
		case 2:
			if (ch >= '0' && ch <= '9') {
				lexState == 2;
			} else {
				lexFoundToken(lexBase, lexPos - lexBase, 1);
				handleLeadingChar(ch, txt);
			}
			break;
		case 3:
			lexFoundToken(lexBase, lexPos - lexBase, 3);
			handleLeadingChar(ch, txt);
			break;
		case 4:
			if (ch == '"')
				lexState = 5;
			else if (ch == '\\') {
				lexState = 6;
			} else if (ch == '\n' || ch == '\r') {
				lexFoundToken(lexBase, lexPos - lexBase, 4);
				handleLeadingChar(ch, txt);
			}
			break;
		case 5:
			lexFoundToken(lexBase, lexPos - lexBase, 4);
			handleLeadingChar(ch, txt);
			break;
		case 6:
			if (ch == '\n' || ch == '\r') {
				lexFoundToken(lexBase, lexPos - lexBase, 4);
				handleLeadingChar(ch, txt);
			} else {
				lexState = 4;
			}
			break;
		case 8:
			if (ch == '\n' || ch == '\r') {
				lexFoundToken(lexBase, lexPos - lexBase, 8);
				handleLeadingChar(ch, txt);
			}
			break;
		case 9:
			if (ch == '*' && txt[1] == '/' && lexPos > lexBase + 1) {
				lexState = 10;
			}
			break;
		case 10:
			lexState = 11;
			break;
		case 11:
			lexFoundToken(lexBase, lexPos - lexBase, 8);
			handleLeadingChar(ch, txt);
			break;
	}
	lexPos++;
}

- (void)textStorageDidProcessEditing:(NSNotification *)aNotification
{
    // get text storage
    NSTextStorage *textstorage = [aNotification object];
    
    // get strings
    NSString *originalString = [textstorage string];
	
	const char *str = [originalString UTF8String];
	
	int pos = 0;
	lexPos = 0;
	lexState = 0;
	lexBase = 0;
	lexStorage = textstorage;
	lexLastHighlight = 0;
	
	while (str[pos]) {
		int ch = (unsigned char) str[pos];
		lexAddCh(ch, str + pos);
		pos++;
		if (ch & 0x80) {
			if ((ch & 0xe0) == 0xc0)
				pos++;
			else if ((ch & 0xf0) == 0xe0)
				pos += 2;
			else
				pos += 3;
		}
	}
		lexComplete();
}
@end
